Skip to content

Inline media block#968

Open
busbyk wants to merge 12 commits intomainfrom
inline-media-block
Open

Inline media block#968
busbyk wants to merge 12 commits intomainfrom
inline-media-block

Conversation

@busbyk
Copy link
Copy Markdown
Collaborator

@busbyk busbyk commented Mar 3, 2026

Description

Adds a new InlineMedia inline block for the Lexical rich text editor, allowing editors to embed images directly within text flow. Images can be positioned inline with configurable vertical alignment, or floated left/right with text wrapping.

Related Issues

None, alternative to #961

Key Changes

  • New InlineMedia block (src/blocks/InlineMedia/) — config and component for inline media in rich text
  • Position options: Inline (within text flow) or float left/right (text wraps around image)
  • Size options: Original (natural size), percentage widths (25%/50%/75%/100%), or fixed pixel height
  • Vertical alignment: Top, middle, bottom, or baseline (only for inline position)
  • Optional caption: Displayed below the image as small gray text
  • Registered in Posts collection and default Lexical editor config as an inlineBlock
  • RichText renderer updated with JSX converter for frontend rendering

How to test

  1. Run pnpm dev and navigate to the admin panel
  2. Create or edit a Post (or any content using the default Lexical editor)
  3. In the rich text editor, use the inline block inserter to add an "Inline Media" block
  4. Upload or select a media item and configure position, size, and alignment
  5. Verify:
    • Inline position: Image renders within the text line with correct vertical alignment
    • Float left/right: Image floats to the side with text wrapping around it
    • Size options: Percentage widths scale relative to the container; fixed height respects the pixel value
    • Caption: Appears as small text below the image when provided
  6. View the published content on the frontend to confirm the rich text JSX converter renders correctly

Screenshots / Demo video

https://www.loom.com/share/a167f8e500824d60b88914fe6b66014b

Hmm I wonder if floats are too confusing to content editors when they can use them, unconstrained in the ContentBlock?
image

Migration Explanation

No migration needed — this adds a new inline block type to existing Lexical rich text fields. Existing content is unaffected.

Future enhancements / Questions

  • Could extend to support inline video embeds
  • May want to add margin/padding controls for finer spacing adjustments around floated images
  • Thumbnails for inline blocks don't seem to work. We likely need to open a PR in Payload for this. I've left imageUrl in the block config using the same MediaThumbnail.jpg for when that gets fixed.
  • Images are still not exactly responsive - if we set a percentage width that width gets used at all screen sizes. Same problem as Media block should use responsive image sizes based on it's container #757. Will fix this block in that issue as well.

busbyk and others added 4 commits March 3, 2026 11:54
Introduces a new inline block for embedding images within rich text content.
Supports float left/right with text wrapping and inline positioning with
configurable vertical alignment. Registered in defaultLexical and Posts collection.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ible caption

Replace small/medium/large presets with more flexible sizing options:
original, 25/50/75/100% container width, and fixed pixel height.
Caption now renders as visible text below the image instead of a tooltip.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 3, 2026

Preview deployment: https://inline-media-block.preview.avy-fx.org

@busbyk busbyk self-assigned this Mar 7, 2026
busbyk and others added 4 commits April 6, 2026 14:08
Create a shared DEFAULT_INLINE_BLOCKS constant (mirroring DEFAULT_BLOCKS pattern)
and register it in all BlocksFeature configs so InlineMedia is available in
Content, Callout, BlogList, HomePages, and Events editors.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Reuse the existing MediaBlock thumbnail for the inline media block inserter.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
defaults.ts imports block configs (e.g. BlogListBlock), and those block
configs now import DEFAULT_INLINE_BLOCKS from defaults.ts, creating a
circular dependency. Move DEFAULT_INLINE_BLOCKS to a separate
defaultInlineBlocks.ts file to break the cycle.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@busbyk busbyk marked this pull request as ready for review April 6, 2026 23:15
@busbyk busbyk requested a review from rchlfryn April 6, 2026 23:15
Copy link
Copy Markdown
Collaborator

@rchlfryn rchlfryn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A lot of thinking out loud as review notes in the PR. Take em or leave em.

To answer your question about having both this and the ImageBlock, I think we should have both and get feedback from users on which makes more sense. I could see users wanting more inline blocks.

defaultValue: 'inline',
options: [
{ label: 'Inline', value: 'inline' },
{ label: 'Float left', value: 'float-left' },
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting - for some reason I thought we were using camel case for values, but every other value is one word.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe update the labels to just be Left and Right? I am not sure float is needed here.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, I'm down for the label change - updated the description too: 61ba101

Interesting - for some reason I thought we were using camel case for values, but every other value is one word.

I almost always default to using snake case for unique values like keys like this or slugs. Just my preference 🤷‍♂️

className="block [&>span]:h-full [&_picture]:h-full [&_picture]:my-0"
style={{ height: `${fixedHeight}px` }}
>
<Media htmlElement="span" resource={media} imgClassName={imgSizeClass} sizes={sizes} />
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason to not include pictureClassName="my-0" here?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah no, good call: 4b352f3

Comment on lines +49 to +52
{ label: '25% width', value: '25' },
{ label: '50% width', value: '50' },
{ label: '75% width', value: '75' },
{ label: '100% width', value: '100' },
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it matters, but maybe we add a w in front of of each value to ensure it's a string. Doesn't seem to be an issue.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the idea, to make it clear that these are strings but we actually use these for the sizes attribute in the component so we'd have to strip off the 'w' prefix which I think would be more confusing.

sizes = `${resolvedSize}vw`
} else if (isFixedHeight) {
imgSizeClass = 'h-full w-auto'
sizes = '96px'
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How is this being calculated/ is it needed?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the attribute — a hint that tells the browser which source to pick from a responsive srcset before layout is known. For fixed-height images the rendered width depends on the image's aspect ratio, which we don't have at render time without reading the media dimensions. 96px is a conservative fallback so the browser doesn't load the full-resolution source. It's imperfect but errs on the side of a smaller download; the browser will still render at the correct size.

const mediaElement = isFixedHeight ? (
<span
className="block [&>span]:h-full [&_picture]:h-full [&_picture]:my-0"
style={{ height: `${fixedHeight}px` }}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⛏️ You could make this a class name like you are above and remove the inline style.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

busbyk and others added 2 commits April 9, 2026 11:23
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The non-fixed-height path already passed this prop directly. The
fixed-height path was achieving the same effect via a descendant
selector, which was inconsistent.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants